Effective Java 2.0_中文版_Item 7

文章作者:Tyan
博客:noahsnail.com | CSDN | 简书

Item 7: 避免使用finalizers(终结方法,Java模拟C++的析构函数)

终结方法通常是不可预测的,经常是危险的,一般来说是没必要的。使用它们会引起不稳定的行为,性能变低,可移植性问题等。终结方法有一些有效的使用,这个在本条目的后面会讲到,但根据经验,你应该避免使用终结方法。

C++程序员被警告说不要去想像Java中模拟C++析构函数那样的终结方法。在C++中,析构函数是一种正常回收对象资源的方式,是构造函数的必要对应。在Java中,当对象不可访问时,垃圾回收器会回收对象的相关资源,不需要程序员进行专门的工作。C++析构函数也用来回收其它的非内存资源。在Java中,try-finally块用来完成这样的功能。

终结方法的一个缺点是不能保证它们及时的执行[JLS,12.6]。从对象变得不可访问开始到它的终结方法被执行结束,这中间的时间可以任意长。这意味着你不应该在终结方法中做任何时间为关键的事情。例如,依赖终结方法来关闭文件是一个严重的错误,因为开放的文件描述符是一种有限的资源。如果许多文件都是打开状态,由于JVM执行终结方法时是迟缓的,因此程序可能失败,因为它不能再打开文件。

尽快执行终结方法是垃圾回收算法的主要功能,在不同的JVM实现中变化很大。依赖终结方法执行及时性的程序同样变化很大。一个程序在测试它的JVM上运行非常完美,但在你最重要客户支持的JVM上它却糟糕地运行失败了,这是完全有可能的。

迟缓终结不仅仅是一个理论问题。在很少的情况下,为一个类提供终结方法可能会随意地延迟它实例的回收。有个同事调试一个长期运行的GUI应用,程序莫名其妙的死掉了,抛出了OutOfMemoryError错误。分析表明在程序死亡时,应用中的终结方法队列中有成千上万的图形对象在等待被终结并回收。遗憾的是,终结方法线程的运行优先级要低于另一个应用线程,因此在另一个应用线程中的对象变得可以被终结时,它们不能被终结。语言规范不能保证哪一个线程来执行终结方法,因此没有轻便的方式来阻止这种问题的发生,除非避免使用终结方法。

不仅语言规范不能保证终结方法及时的执行;而且也不能保证终结方法得到执行。这完全有可能,甚至有可能一个程序终止时,一些不能访问的对象的终结方法都没有执行。结论就是:你从不该依赖终结方法来更新重要的持续状态。例如,依赖一个终结方法来释放一个共享资源,例如数据库,的持续锁,很容易引起整个分布式系统当掉。

不要被System.gcSystem.runFinalization方法诱惑。它们可能会增加终结方法得到执行的几率,但它们不能保证它。能保证终结方法执行的唯一方法是System.runFinalizersOnExit以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这些方法都有致命的缺陷并且已经被废弃了[ThreadStop]。

以防你还不相信终结方法应该被避免,这儿有另一个情况值得思考:如果在终结方法执行期间抛出了一个无法捕获的异常,这个异常被忽略了,对象的终结方法终止了[JLS,12.6]。不能捕获的异常可能会使对象处于崩溃状态。如果另一个线程试图使用这样一个崩溃的对象,任何不确定性的行为都有可能发送。通常,一个未被捕获的异常会终止线程并打印栈轨迹,但如果它发生在一个终结方法中,将不会打印出警告。

哦,还有一件事:使用终结方法会有严重的性能问题。在我的机器上,创建并销毁一个简单对象大约是5.6纳秒。添加一个终结方法会将这个时间增加到2400纳秒。换句话说,创建一个对象并用终结方法销毁对象比正常情况下大约慢了430倍。

因此当一个类的对象封装的资源需要结束时,你应该用什么来代替一个类的终结方法?例如文件或线程?提供一个显式的结束方法,当类的实例不再需要时,要求类的客户端在每个实例上都调用这个方法。一个值得提及的细节是,实例必须跟踪它是否已经被终结:显式的终结方法必须记录在一个私有字段中,这个字段表明对象不再有效,如果其它方法再对象终结后调用对象,其它方法必须检查这个字段并抛出IllegalStateException

显式结束方法的典型例子是InputStreamOutputStreamjava.sql.Connection的关闭方法。另一个例子是java.util.Timercancel方法,它会进行必要的状态检查并一起线程相关的Timer实例平稳的结束它自己。java.awt的例子包括Graphics.disposeWindow.dispose。这些方法经常被忽视,可以预料会引起可怕的性能后果。一个相关的方法是Image.flush,它会释放所有Image实例相关的资源,但会将实例保持在仍可用的状态,如果必要的时候重新分配资源。

显式结束方法通过与try-finally结构结合来确保终结。在finally语句块的内部调用显式的结束方法来确保它得到执行,即使对象使用时抛出了一个异常:

1
2
3
4
5
6
7
8
// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}

那终结方法有什么好处呢?有两种可能的合法应用。一个是作为『安全网』,以防对象拥有者忘记调用它的显式结束方法。但这不能保证终结方法得到及时的调用,当客户端调用显式结束方法失败时,在那种情况下(希望很少),后面释放资源总比不释放资源要好。但终结方法如果发现资源仍没有被释放,它应该输出一个警告,因为这意味着客户端代码存在一个BUG,它应该被修正。如果你正在考虑写这样一个安全网终结方法,要仔细思考这种额外的保护是否值得额外的代价。

作为显式结束方法模式引用的四个例子(FileInputStreamFileOutputStreamTimerConnection)都有终结方法作为安全网以防它们的结束方法没有被调用。遗憾的是这些终结方法不输出警告。这种警告通常在API发布后不能进行添加,因为它会损坏现有的客户端。

终结方法的第二个合法使用是关于对象的本地对等体。本地对等体是一个本地对象,普通对象通过本地方法委托给本地对象。由于本地对等体不是一个正常的对象,当它的Java对等体回收时,垃圾回收器不知道并且不能回收它。假设本地对等体不拥有重要的资源,终结方法是执行这个任务的合适工具。如果本地对等体拥有必须及时终止的资源,这个类应该有一个显式的结束方法,如上所述。结束方法应该用来释放重要资源。结束方法可以是一个本地方法或它可以调用一个本地方法。

很重要的一点就是要注意『终结方法链』是不能自动执行的。如果一个类(不是Object)有一个终结方法,一个子类覆写了它,子类终结方法必须手动调用父类终结方法。你应该try块内终止这个子类并在对应的finally块调用父类终结方法。这保证了父类终结方法得到了执行,即使子类终结方法抛出异常,反之亦然。下面是它的一个例子、注意这个例子使用了Override注解(@Override),在release 1.5版本中添加。现在你可以忽略Override注解,或看Item 36弄明白它是什么意思:

1
2
3
4
5
6
7
8
9
10

// Manual finalizer chaining
@Override
protected void finalize() throws Throwable {
try {
... // Finalize subclass state
} finally {
super.finalize();
}
}
1
2
3
4
5
6
7
8
9
10
11
// Finalizer Guardian idiom
public class Foo {
// Sole purpose of this object is to finalize outer Foo object
private final Object finalizerGuardian = new Object() {
@Override
protected void finalize() throws Throwable {
... // Finalize outer Foo object
}
};
... // Remainder omitted
}

注意公有类Foo没有终结方法(除非它从Object继承一个无关紧要的),因此子类的终结方法是否调用super.finalize是不重要的。每一个含有终结方法的非终结公有类都应该考虑这个技术。

总结:不要使用终结方法,除非是用作安全网或用来终止一个非重要的本地资源。在那些你使用终结方法的稀少实例中,记住调用super.finalize。如果你使用终结方法作为安全网,记住在终结方法中输出非法用法。最后,如果你需要将终结方法关联到一个公有的,非终结类,考虑使用终结方法守护者,即使子类终结方法调用super.finalize失败,也会进行终结。

如果有收获,可以请我喝杯咖啡!